//The MIT License

//Copyright (c) 2009 Lluis Sanchez, Nick Darnell

//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;

using Polymorph.Properties;

namespace Polymorph
{
    /// <summary>
    /// The <see cref="CodeBuilder"/> is the main class developers should use when generating
    /// code for a method.  It provides a simplified interface for creating simple constructs 
    /// like if statements and for loops.
    /// </summary>
    public class CodeBuilder
    {
        CodeBlock mainBlock;
        CodeBlock currentBlock;

        Stack<CodeBlock> m_blockStack = new Stack<CodeBlock>();
        int varId;
        Label returnLabel;
        List<int> m_nestedIfs = new List<int>();

        CodeClass codeClass;

        /// <summary>
        /// Creates an instance of <see cref="CodeBuilder"/>.
        /// </summary>
        public CodeBuilder()
        {
            mainBlock = new CodeBlock();
            currentBlock = mainBlock;
        }

        /// <summary>
        /// Creates an instance of <see cref="CodeBuilder"/>.
        /// </summary>
        /// <param name="block">The block to initialize the <see cref="CodeBuilder"/> with.</param>
        CodeBuilder(CodeBlock block)
        {
            currentBlock = block;
        }

        /// <summary>
        /// Creates an instance of <see cref="CodeBuilder"/>.
        /// </summary>
        /// <param name="codeClass">The <see cref="CodeClass"/> to associate this instance with.</param>
        public CodeBuilder(CodeClass codeClass)
        {
            this.codeClass = codeClass;
            mainBlock = new CodeBlock();
            currentBlock = mainBlock;
        }

        /// <summary>
        /// Gets the current block being constructed.
        /// </summary>
        public CodeBlock CurrentBlock
        {
            get { return currentBlock; }
        }

        /// <summary>
        /// Gets the owner class of the <see cref="CodeBlock"/>.
        /// </summary>
        public CodeClass OwnerClass
        {
            get { return codeClass; }
        }

        /// <summary>
        /// Generates the final IL from all the <see cref="CodeItem"/> objects that have been
        /// added to the <see cref="CodeBuilder"/>.
        /// </summary>
        /// <param name="gen">The <see cref="ILGenerator"/> to emit the IL to.</param>
        public void Generate(ILGenerator gen)
        {
            try
            {
                mainBlock.Generate(gen);
            }
            catch (Exception ex)
            {
                string m = ex.Message + "\nCode block:\n";
                m += "-----------------------\n";
                m += PrintCode();
                m += "-----------------------\n";
                throw new Exception(m, ex);
            }
        }

        /// <summary>
        /// Prints a text representation of the code that is meant to be humanly readable.  It 
        /// resembles C# but has some made-up syntax to express certain things like fault blocks.
        /// </summary>
        /// <returns>The code string.</returns>
        public string PrintCode()
        {
            StringWriter sw = new StringWriter();
            CodeWriter cw = new CodeWriter(sw);
            PrintCode(cw);
            return sw.ToString();
        }

        /// <summary>
        /// Prints the code to the target <see cref="CodeWriter"/>.
        /// </summary>
        /// <param name="cw">The code writer to print to.</param>
        public void PrintCode(CodeWriter cw)
        {
            mainBlock.PrintCode(cw);
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <typeparam name="T">The system <see cref="Type"/> to define the variable 
        /// as.</typeparam>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable<T>()
        {
            return DeclareVariable(typeof(T));
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <param name="type">The system <see cref="Type"/> to define the variable as.</param>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable(Type type)
        {
            return DeclareVariable(type, null);
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <typeparam name="T">The system <see cref="Type"/> to define the variable as.</typeparam>
        /// <param name="ob">The literal value to initialize the variable with.</param>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable<T>(object ob)
        {
            return DeclareVariable(typeof(T), ob);
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <param name="type">The system <see cref="Type"/> to define the variable as.</param>
        /// <param name="ob">The literal value to initialize the variable with.</param>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable(Type type, object ob)
        {
            return DeclareVariable(type, Exp.Literal(ob));
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <param name="initValue">The expression to use to initialize the variable.</param>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable(CodeExpression initValue)
        {
            return DeclareVariable(initValue.ResultType, initValue);
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <typeparam name="T">The system <see cref="Type"/> to define the variable as.</typeparam>
        /// <param name="initValue">The expression to use to initialize the variable.</param>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable<T>(CodeExpression initValue)
        {
            return DeclareVariable(typeof(T), initValue);
        }

        /// <summary>
        /// Declares a new variable in the local scope.
        /// </summary>
        /// <param name="type">The system <see cref="Type"/> to define the variable as.</param>
        /// <param name="initValue">The expression to use to initialize the variable.</param>
        /// <returns>A variable reference wrapper to use when refering to the local variable in 
        /// other expressions.</returns>
        public CodeLocalReference DeclareVariable(Type type, CodeExpression initValue)
        {
            string name = "v" + (varId++);
            CodeVariableDeclaration var = new CodeVariableDeclaration(type, name);
            currentBlock.Add(var);
            if (!object.ReferenceEquals(initValue, null))
                Assign(var.Variable, initValue);
            return var.Variable;
        }

        /// <summary>
        /// Creates an assignment statement, assigning the expression to the variable referenced.
        /// </summary>
        /// <param name="var">The variable reference to assign the expression to.</param>
        /// <param name="val">The expression value to assign to the variable.</param>
        public void Assign(CodeValueReference var, CodeExpression val)
        {
            currentBlock.Add(new CodeAssignment(var, val));
        }

        /// <summary>
        /// Creates an if statement using the provided conditional expression and pushes an if block 
        /// onto the stack.
        /// </summary>
        /// <param name="condition">The conditional expression to place in the if statement.</param>
        public void If(CodeExpression condition)
        {
            currentBlock.Add(new CodeIf(condition));
            PushNewBlock();
            m_nestedIfs.Add(0);
        }

        /// <summary>
        /// Creates an else-if statement using the provided conditional expression and pushes an 
        /// else-if block onto the stack.
        /// </summary>
        /// <param name="condition">The conditional expression to place in the if statement.</param>
        public void ElseIf(CodeExpression condition)
        {
            if (m_nestedIfs.Count == 0)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "ElseIf"));
            }

            Else();
            currentBlock.Add(new CodeIf(condition));
            PushNewBlock();
            m_nestedIfs[m_nestedIfs.Count - 1] = 1 + m_nestedIfs[m_nestedIfs.Count - 1];
        }

        /// <summary>
        /// Creates an else statement and pushes a new block onto the stack.
        /// </summary>
        public void Else()
        {
            CodeBlock block = PopBlock();
            CodeIf cif = currentBlock.GetLastItem() as CodeIf;

            if (cif == null || cif.TrueBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "Else"));
            }

            cif.TrueBlock = block;
            PushNewBlock();
        }

        /// <summary>
        /// Ends a sequence of if-elseif-else statements.
        /// </summary>
        public void EndIf()
        {
            CodeBlock block = PopBlock();
            CodeIf cif = currentBlock.GetLastItem() as CodeIf;

            if (cif == null || cif.FalseBlock != null || m_nestedIfs.Count == 0)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndIf"));
            }

            if (cif.TrueBlock == null)
                cif.TrueBlock = block;
            else
                cif.FalseBlock = block;

            int num = m_nestedIfs[m_nestedIfs.Count - 1];
            if (num > 0)
            {
                m_nestedIfs[m_nestedIfs.Count - 1] = --num;
                EndIf();
            }
            else
            {
                m_nestedIfs.RemoveAt(m_nestedIfs.Count - 1);
            }
        }

        /// <summary>
        /// Begins a select block which should contain a series of Case statements with
        /// conditions to select from.  These case statements are not genuine select-case
        /// blocks and are actually treated more like if-elseif blocks.
        /// </summary>
        public void Switch()
        {
            currentBlock.Add(new CodeSwitch());
            PushNewBlock();
        }

        /// <summary>
        /// Creates a case statement in the currently active Switch block and pushes a case block
        /// onto the stack after popping the previous case block off the stack.
        /// </summary>
        /// <param name="condition">The condition to evaluate in the case block.</param>
        public void Case(CodeExpression condition)
        {
            PopBlock();
            CodeSwitch select = currentBlock.GetLastItem() as CodeSwitch;
            if (select == null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "Case"));
            }

            PushNewBlock();
            select.AddCase(condition, currentBlock);
        }

        /// <summary>
        /// Ends the currently active select block.
        /// </summary>
        public void EndSwitch()
        {
            PopBlock();
            CodeSwitch select = currentBlock.GetLastItem() as CodeSwitch;
            if (select == null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndSwitch"));
            }
        }

        /// <summary>
        /// Begins a while loop.  Pushes a new block onto the stack.
        /// </summary>
        /// <param name="condition">The condition to continue to loop on.</param>
        public void While(CodeExpression condition)
        {
            currentBlock.Add(new CodeWhile(condition));
            PushNewBlock();
        }

        /// <summary>
        /// Ends a while loop.  Pops the while loop block from the stack.
        /// </summary>
        public void EndWhile()
        {
            CodeBlock block = PopBlock();
            CodeWhile cif = currentBlock.GetLastItem() as CodeWhile;

            if (cif == null || cif.WhileBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndWhile"));
            }

            cif.WhileBlock = block;
        }

        public CodeValueReference Foreach<T>(CodeExpression array)
        {
            CodeValueReference item;
            Foreach(typeof(T), out item, array);
            return item;
        }

        public CodeValueReference Foreach(Type type, CodeExpression array)
        {
            CodeValueReference item;
            Foreach(type, out item, array);
            return item;
        }

        public void Foreach<T>(out CodeValueReference item, CodeExpression array)
        {
            Foreach(typeof(T), out item, array);
        }

        public void Foreach(Type type, out CodeValueReference item, CodeExpression array)
        {
            CodeForeach cfe = new CodeForeach(array, type);
            item = cfe.ItemExpression;
            currentBlock.Add(cfe);
            PushNewBlock();
        }

        public void EndForeach()
        {
            CodeBlock block = PopBlock();
            CodeForeach cif = currentBlock.GetLastItem() as CodeForeach;

            if (cif == null || cif.ForBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndForeach"));
            }

            cif.ForBlock = block;
        }

        public void For(CodeExpression initExp, CodeExpression conditionExp, CodeExpression nextExp)
        {
            currentBlock.Add(new CodeFor(initExp, conditionExp, nextExp));
            PushNewBlock();
        }

        public void EndFor()
        {
            CodeBlock block = PopBlock();
            CodeFor cif = currentBlock.GetLastItem() as CodeFor;

            if (cif == null || cif.ForBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndFor"));
            }

            cif.ForBlock = block;
        }

        public void Call(CodeExpression target, string name, params CodeExpression[] parameters)
        {
            currentBlock.Add(new CodeMethodCall(target, name, parameters));
        }

        public void Call(CodeExpression target, MethodInfo method, params CodeExpression[] parameters)
        {
            currentBlock.Add(new CodeMethodCall(target, method, parameters));
        }

        public void Call<T>(string name, params CodeExpression[] parameters)
        {
            Call(typeof(T), name, parameters);
        }

        public void Call(Type type, string name, params CodeExpression[] parameters)
        {
            currentBlock.Add(new CodeMethodCall(type, name, parameters));
        }

        public void Call(MethodInfo method, params CodeExpression[] parameters)
        {
            currentBlock.Add(new CodeMethodCall(method, parameters));
        }

        public void Increment(CodeValueReference val)
        {
            Assign(val, new CodeIncrement(val));
        }

        public void Decrement(CodeValueReference val)
        {
            Assign(val, new CodeDecrement(val));
        }

        public void Return(CodeExpression exp)
        {
            currentBlock.Add(new CodeReturn(this, exp));
        }

        public void Return()
        {
            currentBlock.Add(new CodeReturn(this));
        }

        public void Throw<T>(params CodeExpression[] parameters)
        {
            Throw(typeof(T), parameters);
        }

        public void Throw(Type exception, params CodeExpression[] parameters)
        {
            currentBlock.Add(new CodeThrow(new CodeNew(exception, parameters)));
        }

        public void Try()
        {
            currentBlock.Add(new CodeTry());
            PushNewBlock();
        }

        public void Catch<T>()
        {
            Catch(typeof(T));
        }

        public void Catch(Type exceptionType)
        {
            currentBlock.Add(new CodeCatch(exceptionType));
            PushNewBlock();
        }

        public void EndCatch()
        {
            CodeBlock block = PopBlock();
            CodeCatch codeCatch = currentBlock.GetLastItem() as CodeCatch;

            if (codeCatch == null || codeCatch.CatchBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndCatch"));
            }

            codeCatch.CatchBlock = block;
        }

        public void CatchWhen<T>(CodeExpression condition)
        {
            CatchWhen(typeof(T), condition);
        }

        public void CatchWhen(Type exceptionType, CodeExpression condition)
        {
            currentBlock.Add(new CodeCatchWhen(exceptionType, condition));
            PushNewBlock();
        }

        public void EndCatchWhen()
        {
            CodeBlock block = PopBlock();
            CodeCatchWhen codeCatchWhen = currentBlock.GetLastItem() as CodeCatchWhen;

            if (codeCatchWhen == null || codeCatchWhen.CatchBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndCatchWhen"));
            }

            codeCatchWhen.CatchBlock = block;
        }

        public void Fault()
        {
            currentBlock.Add(new CodeFault());
            PushNewBlock();
        }

        public void EndFault()
        {
            CodeBlock block = PopBlock();
            CodeFault codeFault = currentBlock.GetLastItem() as CodeFault;

            if (codeFault == null || codeFault.FaultBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndFault"));
            }

            codeFault.FaultBlock = block;
        }

        public void Finally()
        {
            currentBlock.Add(new CodeFinally());
            PushNewBlock();
        }

        public void EndFinally()
        {
            CodeBlock block = PopBlock();
            CodeFinally codeFinally = currentBlock.GetLastItem() as CodeFinally;

            if (codeFinally == null || codeFinally.FinallyBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndFinally"));
            }

            codeFinally.FinallyBlock = block;
        }

        public void EndTry()
        {
            CodeBlock block = PopBlock();
            CodeTry ct = currentBlock.GetLastItem() as CodeTry;

            if (ct == null || ct.TryBlock != null)
            {
                throw new InvalidOperationException(
                    string.Format(Resources.A0NotAllowedHere, "EndTry"));
            }

            ct.TryBlock = block;
        }

        /// <summary>
        /// Adds a no-op operator to the current block.
        /// </summary>
        [Conditional("DEBUG")]
        public void NoOp()
        {
            currentBlock.Add(new CodeNoOp());
        }

        /// <summary>
        /// Appends the <see cref="CodeItem"/> to the <see cref="CodeBuilder"/>'s current block.
        /// </summary>
        /// <param name="item">The <see cref="CodeItem"/> to append.</param>
        /// <returns>The <see cref="CodeBuilder"/> that was appended to.</returns>
        public CodeBuilder Append(CodeItem item)
        {
            currentBlock.Add(item);
            return this;
        }

        /// <summary>
        /// Appends the code item to the <see cref="CodeBuilder"/>'s current block.
        /// </summary>
        /// <param name="cb">The <see cref="CodeBuilder"/> to append the <see cref="CodeItem"/> 
        /// to.</param>
        /// <param name="item">The <see cref="CodeItem"/> to append.</param>
        /// <returns>The <see cref="CodeBuilder"/> that was appended to.</returns>
        public static CodeBuilder operator +(CodeBuilder cb, CodeItem item)
        {
            return cb.Append(item);
        }

        internal Label ReturnLabel
        {
            get { return returnLabel; }
            set { returnLabel = value; }
        }

        private void PushNewBlock()
        {
            m_blockStack.Push(currentBlock);
            currentBlock = new CodeBlock();
        }

        private CodeBlock PopBlock()
        {
            CodeBlock block = currentBlock;
            currentBlock = m_blockStack.Pop();
            return block;
        }
    }
}